package labox.innovation.gameserver.pathfinding.geonodes;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.LineNumberReader;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javolution.util.FastList;
import javolution.util.FastMap;
import labox.innovation.config.Config;
import labox.innovation.gameserver.geodata.GeoData;
import labox.innovation.gameserver.model.FuzeWorld;
import labox.innovation.gameserver.model.Location;
import labox.innovation.gameserver.pathfinding.Node;
import labox.innovation.gameserver.pathfinding.PathFinding;

/**
 * ��·���Ϊ���Ѱ·ģ�飬��ߵ��ڴ�ʹ�ã���͵�CPU���
 * 
 * @author -Nemesiss-
 */
public class GeoPathFinding extends PathFinding {

	private static final Logger _log = LoggerFactory.getLogger(GeoPathFinding.class.getName());

	private static Map<Short, ByteBuffer> _pathNodes = new FastMap<Short, ByteBuffer>();

	private static Map<Short, IntBuffer> _pathNodesIndex = new FastMap<Short, IntBuffer>();

	public static GeoPathFinding getInstance() {
		return SingletonHolder._instance;
	}

	/**
	 * @see labox.innovation.gameserver.pathfinding.PathFinding#PathNodesExist(short)
	 */
	@Override
	public boolean pathNodesExist(short regionoffset) {
		return _pathNodesIndex.containsKey(regionoffset);
	}

	/**
	 * @see labox.innovation.gameserver.pathfinding.PathFinding#FindPath(int, int, short, int, int, short)
	 */
	@Override
	public Node findPath(int id, int x, int y, int tx, int ty) {
		int gx = x >> 4;
		int gy = y >> 4;

		int gtx = tx >> 4;
		int gty = ty >> 4;

		GeoData geo = FuzeWorld.getInstance().getScene(id).getGeoData();

		Node start = readNode(geo, gx, gy);
		Node end = readNode(geo, gtx, gty);
		if (start == null || end == null)
			return null;

		if (start == end)
			return null;

		// TODO: Find closest path node we CAN access. Now only checks if we can
		// not reach the closest
		Location temp = geo.moveCheck(x, y, start.getLoc().getX(), start.getLoc().getY());
		if ((temp.getX() != start.getLoc().getX()) || (temp.getY() != start.getLoc().getY()))
			return null; // cannot reach closest...

		// TODO: Find closest path node around target, now only checks if final
		// location can be reached
		temp = geo.moveCheck(tx, ty, end.getLoc().getX(), end.getLoc().getY());
		if ((temp.getX() != end.getLoc().getX()) || (temp.getY() != end.getLoc().getY()))
			return null; // cannot reach closest...

		// return searchAStar(start, end);
		return searchByClosest2(start, end);
	}

	/**
	 * @see labox.innovation.gameserver.pathfinding.PathFinding#ReadNeighbors(short, short)
	 */
	@Override
	public Node[] readNeighbors(Node n, GeoData geo) {
		int node_x = n.getLoc().getNodeX();
		int node_y = n.getLoc().getNodeY();
		// short node_z = n.getLoc().getZ();

		short regoffset = getRegionOffset(getRegionX(node_x), getRegionY(node_y));
		ByteBuffer pn = _pathNodes.get(regoffset);

		List<Node> Neighbors = new FastList<Node>(8);
		Node newNode;
		short new_node_x, new_node_y;

		Node[] result = new Node[Neighbors.size()];
		return Neighbors.toArray(result);
	}

	// Private

	private Node readNode(GeoData geo, short node_x, short node_y) {
		short regoffset = getRegionOffset(getRegionX(node_x), getRegionY(node_y));
		if (!this.pathNodesExist(regoffset))
			return null;
		short nbx = getNodeBlock(node_x);
		short nby = getNodeBlock(node_y);
		int idx = _pathNodesIndex.get(regoffset).get((nby << 8) + nbx);
		ByteBuffer pn = _pathNodes.get(regoffset);
		// reading
		byte nodes = pn.get(idx);

		short node_z = pn.getShort(idx);
		idx += 2;
		return new Node(new GeoNodeLoc(node_x, node_y), geo);
	}

	private Node readNode(GeoData geo, int gx, int gy) {
		short node_x = getNodePos(gx);
		short node_y = getNodePos(gy);
		return new Node(new GeoNodeLoc(node_x, node_y), geo);
	}

	private GeoPathFinding() {
		LineNumberReader lnr = null;
		try {
			_log.info("PathFinding Engine: - Loading Path Nodes...", Config.SERVER_ID);
			File Data = new File("./data/pathnode/pn_index.txt");
			if (!Data.exists())
				return;

			lnr = new LineNumberReader(new BufferedReader(new FileReader(Data)));
		} catch (Exception e) {
			e.printStackTrace();
			throw new Error("Failed to Load pn_index File.");
		}
		String line;
		try {
			while ((line = lnr.readLine()) != null) {
				if (line.trim().length() == 0)
					continue;
				StringTokenizer st = new StringTokenizer(line, "_");
				byte rx = Byte.parseByte(st.nextToken());
				byte ry = Byte.parseByte(st.nextToken());
				LoadPathNodeFile(rx, ry);
			}
		} catch (Exception e) {
			e.printStackTrace();
			throw new Error("Failed to Read pn_index File.");
		} finally {
			try {
				lnr.close();
			} catch (Exception e) {
			}
		}
	}

	private void LoadPathNodeFile(byte rx, byte ry) {
		String fname = "./data/pathnode/" + rx + "_" + ry + ".pn";
		short regionoffset = getRegionOffset(rx, ry);
		_log.info("PathFinding Engine: - Loading: " + fname + " -> region offset: " + regionoffset + "X: " + rx + " Y: " + ry, Config.SERVER_ID);
		File Pn = new File(fname);
		int node = 0, size, index = 0;
		FileChannel roChannel = null;
		try {
			// Create a read-only memory-mapped file
			roChannel = new RandomAccessFile(Pn, "r").getChannel();
			size = (int) roChannel.size();
			MappedByteBuffer nodes;
			if (Config.FORCE_GEODATA) // Force O/S to Loads this buffer's
				// content into physical memory.
				// it is not guarantee, because the underlying operating system
				// may have paged out some of the buffer's data
				nodes = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, size).load();
			else
				nodes = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);

			// Indexing pathnode files, so we will know where each block starts
			IntBuffer indexs = IntBuffer.allocate(65536);

			while (node < 65536) {
				byte layer = nodes.get(index);
				indexs.put(node++, index);
				index += layer * 10 + 1;
			}
			_pathNodesIndex.put(regionoffset, indexs);
			_pathNodes.put(regionoffset, nodes);
		} catch (Exception e) {
			e.printStackTrace();
			_log.error("Failed to Load PathNode File: " + fname, Config.SERVER_ID, e);
		} finally {
			try {
				roChannel.close();
			} catch (Exception e) {
			}
		}

	}

	/**
	 * Convert geodata position to pathnode position
	 * 
	 * @param geo_pos
	 * @return pathnode position
	 */
	public short getNodePos(int geo_pos) {
		return (short) (geo_pos >> 3); // OK?
	}

	/**
	 * Convert node position to pathnode block position
	 * 
	 * @param geo_pos
	 * @return pathnode block position (0...255)
	 */
	public short getNodeBlock(int node_pos) {
		return (short) (node_pos % 256);
	}

	public byte getRegionX(int node_pos) {
		return (byte) ((node_pos >> 8) + 10);
	}

	public byte getRegionY(int node_pos) {
		return (byte) ((node_pos >> 8) + 10);
	}

	public short getRegionOffset(byte rx, byte ry) {
		return (short) ((rx << 5) + ry);
	}

	/**
	 * Convert pathnode x to World x position
	 * 
	 * @param node_x
	 *            , rx
	 * @return
	 */
	public int calculateWorldX(short node_x) {
		return node_x * 128 + 48;
	}

	/**
	 * Convert pathnode y to World y position
	 * 
	 * @param node_y
	 * @return
	 */
	public int calculateWorldY(short node_y) {
		return node_y * 128 + 48;
	}

	@SuppressWarnings("synthetic-access")
	private static class SingletonHolder {
		protected static final GeoPathFinding _instance = new GeoPathFinding();
	}
}
